package org.ru.mse10.cvis.web.validator;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ResourceBundle;

import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpSession;

import org.richfaces.validator.MessageFactory;
import org.ru.mse10.cvis.web.bean.action.login.UserLoginAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Phase Listener that checks the viewId (URL) against a set of filters to
 * determine the required access level. If the correct level is not there then
 * redirect.
 * 
 * @author Spawn
 * 
 */
public class LoginCheckValidator implements PhaseListener {

	/** Logger for this class. */
	private static final Logger log = LoggerFactory
			.getLogger(LoginCheckValidator.class);

	/** The context. */
	private final FacesContext context = FacesContext.getCurrentInstance();

	/** The bundle. */
	private final ResourceBundle bundle = context.getApplication()
			.getResourceBundle(context, "message");

	/** The Constant serialVersionUID. */
	private static final long serialVersionUID = 1L;

	/** The level filters. */
	private final HashMap<AccessLevel, List<UrlFilter>> levelFilters = new HashMap<AccessLevel, List<UrlFilter>>();

	/**
	 * The Enum AccessLevel.
	 */
	public enum AccessLevel {

		/** The none. */
		NONE,
		/** The logged in. */
		LOGGED_IN,
		/** The user active. */
		USER_ACTIVE,
		/** The admin. */
		ADMIN;
	}

	/**
	 * Instantiates a new login check validator.
	 */
	public LoginCheckValidator() {
		initLevels();

		requires(AccessLevel.LOGGED_IN).include("*").exclude("/pages/login/*")
				.exclude("/pages/help/*");

		requires(AccessLevel.USER_ACTIVE).include("/pages/administration/*");

	}

	/**
	 * Inits the levels.
	 */
	private void initLevels() {
		AccessLevel[] levels = AccessLevel.values();
		for (int i = 1; i < levels.length; i++) {
			levelFilters.put(levels[i], new ArrayList<UrlFilter>());
		}
	}

	/**
	 * Requires.
	 * 
	 * @param level
	 *            the level
	 * @return the url filter
	 */
	private UrlFilter requires(AccessLevel level) {
		// ALL is default
		if (level == AccessLevel.NONE)
			return null;

		UrlFilter filter = new UrlFilter();
		List<UrlFilter> list = levelFilters.get(level);
		list.add(filter);
		return filter;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * javax.faces.event.PhaseListener#afterPhase(javax.faces.event.PhaseEvent)
	 */
	@Override
	public void afterPhase(PhaseEvent event) {
		try {
			// check have correct access
			FacesContext context = event.getFacesContext();
			HttpSession session = (HttpSession) context.getExternalContext()
					.getSession(true);
			// UserLoginAction userLoginAction = (UserLoginAction) session
			// .getAttribute(SESSION_BEAN);
			ExpressionFactory expressionFactory = context.getApplication()
					.getExpressionFactory();
			ValueExpression valueExpression = expressionFactory
					.createValueExpression(context.getELContext(),
							"#{loginAction}", UserLoginAction.class);
			UserLoginAction userLoginAction = (UserLoginAction) valueExpression
					.getValue((context.getELContext()));
			// boolean logResults = ls.isHasLoggedIn();
			if (userLoginAction == null) {
				log.error("Could not obtain instance of sessionBean");
				return;
			}

			// can't use this here. only valid at render response phase?
			String viewId = context.getViewRoot().getViewId();
			AccessLevel required = requiredLevel(viewId);
			log.debug("Required level={} for viewId={}", required, viewId);

			// check if page require access:
			switch (required) {
			case NONE:
				break;
			case LOGGED_IN:
				if (!userLoginAction.isLoggedIn())
					redirectLogin(event.getFacesContext(), userLoginAction);
				break;
			case USER_ACTIVE:
				if (!userLoginAction.isActive())
					redirectActive(event.getFacesContext());
				break;
			case ADMIN:
				if (!userLoginAction.isAdmin())
					redirectAdmin(event.getFacesContext());
				break;
			default:
				// error
				log.error("huh?");
				throw new IllegalArgumentException("Not a valid access level");
			}
		} catch (Exception e) {
			log.error("beforePhase caught exception", e);
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * javax.faces.event.PhaseListener#beforePhase(javax.faces.event.PhaseEvent)
	 */
	@Override
	public void beforePhase(PhaseEvent event) {

	}

	/**
	 * Redirect login.
	 * 
	 * @param context
	 *            the context
	 * @param userLoginAction
	 *            the user login action
	 */
	private void redirectLogin(FacesContext context,
			UserLoginAction userLoginAction) {
		// trigger login popup to be shown on render.
		userLoginAction.initLogin();
		addError(context, "logging error");
		context.getApplication().getNavigationHandler()
				.handleNavigation(context, null, "loginPage");
	}

	/**
	 * Redirect active.
	 * 
	 * @param context
	 *            the context
	 */
	private void redirectActive(FacesContext context) {
		addError(context, "active error");
		context.getApplication().getNavigationHandler()
				.handleNavigation(context, null, "loginPage");
	}

	/**
	 * Redirect admin.
	 * 
	 * @param context
	 *            the context
	 */
	private void redirectAdmin(FacesContext context) {
		addError(context, "admin error");
		context.getApplication().getNavigationHandler()
				.handleNavigation(context, null, "home");
	}

	/**
	 * Add keyed error/message.
	 * 
	 * @param context
	 *            the context
	 * @param key
	 *            message key
	 */
	private void addError(FacesContext context, String key) {
		FacesMessage fMessage = MessageFactory.createMessage(context, key);
		if (fMessage != null) {
			FacesContext facesContext = FacesContext.getCurrentInstance();
			fMessage.setSeverity(FacesMessage.SEVERITY_ERROR);
			// send conversation message to login form. Must refix this here.
			// facesContext.addMessage("loginForm", fMessage);
		}
	}

	/**
	 * Checks defined filters for view id, checks starting at the highest level
	 * down to NONE.
	 * 
	 * @param viewId
	 *            the view id
	 * @return the matching level or {@link AccessLevel#NONE} if none matching.
	 */
	private AccessLevel requiredLevel(String viewId) {
		AccessLevel[] levels = AccessLevel.values();
		for (int i = levels.length - 1; i > 0; i--) {
			if (checkLevel(levels[i], viewId))
				return levels[i];
		}

		return AccessLevel.NONE;
	}

	/**
	 * Check level.
	 * 
	 * @param level
	 *            the level
	 * @param viewId
	 *            the view id
	 * @return true, if successful
	 */
	private boolean checkLevel(AccessLevel level, String viewId) {
		return matchUri(levelFilters.get(level), viewId);
	}

	/**
	 * Match uri.
	 * 
	 * @param list
	 *            the list
	 * @param uri
	 *            the uri
	 * @return true, if successful
	 */
	private boolean matchUri(List<UrlFilter> list, String uri) {
		for (UrlFilter filter : list) {
			if (filter.matches(uri))
				return true;
		}
		return false;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.faces.event.PhaseListener#getPhaseId()
	 */
	@Override
	public PhaseId getPhaseId() {
		// ALL access go through RESTORE_VIEW and RENDER_VIEW (even direct url)
		return PhaseId.RESTORE_VIEW;
	}

}